<!DOCTYPE html>
<html lang="zh-cn">
<head>
  <title>Cookie 与 Session - 为企业级框架和应用而生</title>
  <meta charset="utf-8">
  <meta name="description" content="index.description">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  <link rel="icon" href="/images/favicon.png" type="image/x-icon">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/docsearch.js/2/docsearch.min.css" />
<link rel="stylesheet" href="/css/index.css">

    <script>
    !function(t,e,a,r,c){t.TracertCmdCache=t.TracertCmdCache||[],t[c]=window[c]||
      {_isInit:!0,call:function(){t.TracertCmdCache.push(arguments)},
      start:function(t){this.call('start',t)}},t[c].l=new Date;
      var n=e.createElement(a),s=e.getElementsByTagName(a)[0];
      n.async=!0,n.src=r,s.parentNode.insertBefore(n,s)}
    (window,document,'script','https://tracert.alipay.com/tracert.js','Tracert');
      Tracert.start({
        plugins: [ 'BucName' ],
        spmAPos: 'a454',
        spmBPos: 'b4893',
      });
    </script>
  
</head>
<body>
  <div class="nav" >
  <header>
    <a href="/zh-cn/" class="nav-logo leftpadding" alt="egg"><img src="https://zos.alipayobjects.com/rmsportal/VTcUYAaoKqXyHJbLAPyF.svg"></a>
    <ul class="nav-item">
      <li>
        <form id="search-form">
          <input type="text" id="search-query" class="search-query st-default-search-input">
        </form>
      </li>
      <li><a href="/zh-cn/intro/" alt="指南">指南</a></li><li><a href="/api/" alt="API">API</a></li><li><a href="/zh-cn/tutorials/index.html" alt="教程">教程</a></li><li><a href="https://github.com/search?q=topic%3Aegg-plugin&type=Repositories" alt="插件">插件</a></li><li><a href="https://github.com/eggjs/egg/releases" alt="发布日志">发布日志</a></li>
      
      
        <li class="translations">
          <a class="nav-link">Translations</a>
          <span class="arrow"></span><ul id="dropdownContent" class="dropdown-content"><li><a id="en" href="/en/core/cookie-and-session.html" >English</a></li><li><a id="zh-cn" href="/zh-cn/core/cookie-and-session.html" style="color: #22ab28">中文</a></li></ul>
        </li>
      
      <li><iframe src="https://ghbtns.com/github-btn.html?user=eggjs&repo=egg&type=star&count=true" frameborder="0" scrolling="0" width="150px" height="20px"></iframe></li>
    </ul>
    <a id="mobileTrigger" href="#" class="mobile-trigger">
      <ul>
        <li></li>
        <li></li>
        <li></li>
      </ul>
    </a>
  </header>
</div>
  <div id="container" class="container">
    <div class="page-main">
  <article class="markdown-body">
    <h1>Cookie 与 Session</h1>
    <h2 id="cookie"><a class="markdown-anchor" href="#cookie">#</a> Cookie</h2>
<p>HTTP 请求都是无状态的，但是我们的 Web 应用通常都需要知道发起请求的人是谁。为了解决这个问题，HTTP 协议设计了一个特殊的请求头：<a href="https://en.wikipedia.org/wiki/HTTP_cookie" target="_blank" rel="noopener">Cookie</a>。服务端可以通过响应头（set-cookie）将少量数据响应给客户端，浏览器会遵循协议将数据保存，并在下次请求同一个服务的时候带上（浏览器也会遵循协议，只在访问符合 Cookie 指定规则的网站时带上对应的 Cookie 来保证安全性）。</p>
<p>通过 <code>ctx.cookies</code>，我们可以在 controller 中便捷、安全的设置和读取 Cookie。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">HomeController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> add() &#123;</span><br><span class="line">    <span class="keyword">const</span> ctx = <span class="keyword">this</span>.ctx;</span><br><span class="line">    <span class="keyword">let</span> count = ctx.cookies.get(<span class="string">'count'</span>);</span><br><span class="line">    count = count ? <span class="built_in">Number</span>(count) : <span class="number">0</span>;</span><br><span class="line">    ctx.cookies.set(<span class="string">'count'</span>, ++count);</span><br><span class="line">    ctx.body = count;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">async</span> remove() &#123;</span><br><span class="line">    <span class="keyword">const</span> ctx = <span class="keyword">this</span>.ctx;</span><br><span class="line">    ctx.cookies.set(<span class="string">'count'</span>, <span class="literal">null</span>);</span><br><span class="line">    ctx.status = <span class="number">204</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h4 id="ctxcookiessetkey-value-options"><a class="markdown-anchor" href="#ctxcookiessetkey-value-options">#</a> <code>ctx.cookies.set(key, value, options)</code></h4>
<p>设置 Cookie 其实是通过在 HTTP 响应中设置 set-cookie 头完成的，每一个 set-cookie 都会让浏览器在 Cookie 中存一个键值对。在设置 Cookie 值的同时，协议还支持许多参数来配置这个 Cookie 的传输、存储和权限。</p>
<ul>
<li><code>{Number} maxAge</code>: 设置这个键值对在浏览器的最长保存时间。是一个从服务器当前时刻开始的毫秒数。</li>
<li><code>{Date} expires</code>: 设置这个键值对的失效时间，如果设置了 maxAge，expires 将会被覆盖。如果 maxAge 和 expires 都没设置，Cookie 将会在浏览器的会话失效（一般是关闭浏览器时）的时候失效。</li>
<li><code>{String} path</code>: 设置键值对生效的 URL 路径，默认设置在根路径上（<code>/</code>），也就是当前域名下的所有 URL 都可以访问这个 Cookie。</li>
<li><code>{String} domain</code>: 设置键值对生效的域名，默认没有配置，可以配置成只在指定域名才能访问。</li>
<li><code>{Boolean} httpOnly</code>: 设置键值对是否可以被 js 访问，默认为 true，不允许被 js 访问。</li>
<li><code>{Boolean} secure</code>: 设置键值对<a href="http://stackoverflow.com/questions/13729749/how-does-cookie-secure-flag-work" target="_blank" rel="noopener">只在 HTTPS 连接上传输</a>，框架会帮我们判断当前是否在 HTTPS 连接上自动设置 secure 的值。</li>
</ul>
<p>除了这些属性之外，框架另外扩展了 3 个参数的支持：</p>
<ul>
<li><code>{Boolean} overwrite</code>：设置 key 相同的键值对如何处理，如果设置为 true，则后设置的值会覆盖前面设置的，否则将会发送两个 set-cookie 响应头。</li>
<li><code>{Boolean} signed</code>：设置是否对 Cookie 进行签名，如果设置为 true，则设置键值对的时候会同时对这个键值对的值进行签名，后面取的时候做校验，可以防止前端对这个值进行篡改。默认为 true。</li>
<li><code>{Boolean} encrypt</code>：设置是否对 Cookie 进行加密，如果设置为 true，则在发送 Cookie 前会对这个键值对的值进行加密，客户端无法读取到 Cookie 的明文值。默认为 false。</li>
</ul>
<p>在设置 Cookie 时我们需要思考清楚这个 Cookie 的作用，它需要被浏览器保存多久？是否可以被 js 获取到？是否可以被前端修改？</p>
<p><strong>默认的配置下，Cookie 是加签不加密的，浏览器可以看到明文，js 不能访问，不能被客户端（手工）篡改。</strong></p>
<ul>
<li>如果想要 Cookie 在浏览器端可以被 js 访问并修改:</li>
</ul>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line">ctx.cookies.set(key, value, &#123;</span><br><span class="line">  httpOnly: <span class="literal">false</span>,</span><br><span class="line">  signed: <span class="literal">false</span>,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<ul>
<li>如果想要 Cookie 在浏览器端不能被修改，不能看到明文：</li>
</ul>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line">ctx.cookies.set(key, value, &#123;</span><br><span class="line">  httpOnly: <span class="literal">true</span>, <span class="comment">// 默认就是 true</span></span><br><span class="line">  encrypt: <span class="literal">true</span>, <span class="comment">// 加密传输</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p>注意：</p>
<ol>
<li>由于<a href="http://stackoverflow.com/questions/7567154/can-i-use-unicode-characters-in-http-headers" target="_blank" rel="noopener">浏览器和其他客户端实现的不确定性</a>，为了保证 Cookie 可以写入成功，建议 value 通过 base64 编码或者其他形式 encode 之后再写入。</li>
<li>由于<a href="http://stackoverflow.com/questions/640938/what-is-the-maximum-size-of-a-web-browsers-cookies-key" target="_blank" rel="noopener">浏览器对 Cookie 有长度限制限制</a>，所以尽量不要设置太长的 Cookie。一般来说不要超过 4093 bytes。当设置的 Cookie value 大于这个值时，框架会打印一条警告日志。</li>
</ol>
<h4 id="ctxcookiesgetkey-options"><a class="markdown-anchor" href="#ctxcookiesgetkey-options">#</a> <code>ctx.cookies.get(key, options)</code></h4>
<p>由于 HTTP 请求中的 Cookie 是在一个 header 中传输过来的，通过框架提供的这个方法可以快速的从整段 Cookie 中获取对应的键值对的值。上面在设置 Cookie 的时候，我们可以设置 <code>options.signed</code> 和 <code>options.encrypt</code> 来对 Cookie 进行签名或加密，因此对应的在获取 Cookie 的时候也要传相匹配的选项。</p>
<ul>
<li>如果设置的时候指定为 signed，获取时未指定，则不会在获取时对取到的值做验签，导致可能被客户端篡改。</li>
<li>如果设置的时候指定为 encrypt，获取时未指定，则无法获取到真实的值，而是加密过后的密文。</li>
</ul>
<p>如果要获取前端或者其他系统设置的 cookie，需要指定参数 <code>signed</code> 为 <code>false</code>，避免对它做验签导致获取不到 cookie 的值。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line">ctx.cookies.get(<span class="string">'frontend-cookie'</span>, &#123;</span><br><span class="line">  signed: <span class="literal">false</span>,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<h3 id="cookie-秘钥"><a class="markdown-anchor" href="#cookie-秘钥">#</a> Cookie 秘钥</h3>
<p>由于我们在 Cookie 中需要用到加解密和验签，所以需要配置一个秘钥供加密使用。在 <code>config/config.default.js</code> 中</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">  keys: <span class="string">'key1,key2'</span>,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>keys 配置成一个字符串，可以按照逗号分隔配置多个 key。Cookie 在使用这个配置进行加解密时：</p>
<ul>
<li>加密和加签时只会使用第一个秘钥。</li>
<li>解密和验签时会遍历 keys 进行解密。</li>
</ul>
<p>如果我们想要更新 Cookie 的秘钥，但是又不希望之前设置到用户浏览器上的 Cookie 失效，可以将新的秘钥配置到 keys 最前面，等过一段时间之后再删去不需要的秘钥即可。</p>
<h2 id="session"><a class="markdown-anchor" href="#session">#</a> Session</h2>
<p>Cookie 在 Web 应用中经常承担标识请求方身份的功能，所以 Web 应用在 Cookie 的基础上封装了 Session 的概念，专门用做用户身份识别。</p>
<p>框架内置了 <a href="https://github.com/eggjs/egg-session" target="_blank" rel="noopener">Session</a> 插件，给我们提供了 <code>ctx.session</code> 来访问或者修改当前用户 Session 。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">HomeController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> fetchPosts() &#123;</span><br><span class="line">    <span class="keyword">const</span> ctx = <span class="keyword">this</span>.ctx;</span><br><span class="line">    <span class="comment">// 获取 Session 上的内容</span></span><br><span class="line">    <span class="keyword">const</span> userId = ctx.session.userId;</span><br><span class="line">    <span class="keyword">const</span> posts = <span class="keyword">await</span> ctx.service.post.fetch(userId);</span><br><span class="line">    <span class="comment">// 修改 Session 的值</span></span><br><span class="line">    ctx.session.visited = ctx.session.visited ? ctx.session.visited++ : <span class="number">1</span>;</span><br><span class="line">    ctx.body = &#123;</span><br><span class="line">      success: <span class="literal">true</span>,</span><br><span class="line">      posts,</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>Session 的使用方法非常直观，直接读取它或者修改它就可以了，如果要删除它，直接将它赋值为 null：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line">ctx.session = <span class="literal">null</span>;</span><br></pre></td></tr></table></figure>
<p>Session 的实现是基于 Cookie 的，默认配置下，用户 Session 的内容加密后直接存储在 Cookie 中的一个字段中，用户每次请求我们网站的时候都会带上这个 Cookie，我们在服务端解密后使用。Session 的默认配置如下：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line">exports.session = &#123;</span><br><span class="line">  key: <span class="string">'EGG_SESS'</span>,</span><br><span class="line">  maxAge: <span class="number">24</span> * <span class="number">3600</span> * <span class="number">1000</span>, <span class="comment">// 1 天</span></span><br><span class="line">  httpOnly: <span class="literal">true</span>,</span><br><span class="line">  encrypt: <span class="literal">true</span>,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>可以看到这些参数除了 <code>key</code> 都是 Cookie 的参数，<code>key</code> 代表了存储 Session 的 Cookie 键值对的 key 是什么。在默认的配置下，存放 Session 的 Cookie 将会加密存储、不可被前端 js 访问，这样可以保证用户的 Session 是安全的。</p>
<h3 id="扩展存储"><a class="markdown-anchor" href="#扩展存储">#</a> 扩展存储</h3>
<p>Session 默认存放在 Cookie 中，但是如果我们的 Session 对象过于庞大，就会带来一些额外的问题：</p>
<ul>
<li>前面提到，浏览器通常都有限制最大的 Cookie 长度，当设置的 Session 过大时，浏览器可能拒绝保存。</li>
<li>Cookie 在每次请求时都会带上，当 Session 过大时，每次请求都要额外带上庞大的 Cookie 信息。</li>
</ul>
<p>框架提供了将 Session 存储到除了 Cookie 之外的其他存储的扩展方案，我们只需要设置 <code>app.sessionStore</code> 即可将 Session 存储到指定的存储中。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app.js</span></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="params">app</span> =&gt;</span> &#123;</span><br><span class="line">  app.sessionStore = &#123;</span><br><span class="line">    <span class="comment">// support promise / async</span></span><br><span class="line">    <span class="keyword">async</span> get (key) &#123;</span><br><span class="line">      <span class="comment">// return value;</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="keyword">async</span> set (key, value, maxAge) &#123;</span><br><span class="line">      <span class="comment">// set key to store</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="keyword">async</span> destroy (key) &#123;</span><br><span class="line">      <span class="comment">// destroy key</span></span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>sessionStore 的实现我们也可以封装到插件中，例如 <a href="https://github.com/eggjs/egg-session-redis" target="_blank" rel="noopener">egg-session-redis</a> 就提供了将 Session 存储到 redis 中的能力，在应用层，我们只需要引入 <a href="https://github.com/eggjs/egg-redis" target="_blank" rel="noopener">egg-redis</a> 和 <a href="https://github.com/eggjs/egg-session-redis" target="_blank" rel="noopener">egg-session-redis</a> 插件即可。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// plugin.js</span></span><br><span class="line">exports.redis = &#123;</span><br><span class="line">  enable: <span class="literal">true</span>,</span><br><span class="line">  package: <span class="string">'egg-redis'</span>,</span><br><span class="line">&#125;;</span><br><span class="line">exports.sessionRedis = &#123;</span><br><span class="line">  enable: <span class="literal">true</span>,</span><br><span class="line">  package: <span class="string">'egg-session-redis'</span>,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p><strong>注意：一旦选择了将 Session 存入到外部存储中，就意味着系统将强依赖于这个外部存储，当它挂了的时候，我们就完全无法使用 Session 相关的功能了。因此我们更推荐大家只将必要的信息存储在 Session 中，保持 Session 的精简并使用默认的 Cookie 存储，用户级别的缓存不要存储在 Session 中。</strong></p>
<h3 id="session-实践"><a class="markdown-anchor" href="#session-实践">#</a> Session 实践</h3>
<h4 id="修改用户-session-失效时间"><a class="markdown-anchor" href="#修改用户-session-失效时间">#</a> 修改用户 Session 失效时间</h4>
<p>虽然在 Session 的配置中有一项是 maxAge，但是它只能全局设置 Session 的有效期，我们经常可以在一些网站的登陆页上看到有 <strong>记住我</strong> 的选项框，勾选之后可以让登陆用户的 Session 有效期更长。这种针对特定用户的 Session 有效时间设置我们可以通过 <code>ctx.session.maxAge=</code> 来实现。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> ms = <span class="built_in">require</span>(<span class="string">'ms'</span>);</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">UserController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> login() &#123;</span><br><span class="line">    <span class="keyword">const</span> ctx = <span class="keyword">this</span>.ctx;</span><br><span class="line">    <span class="keyword">const</span> &#123; username, password, rememberMe &#125; = ctx.request.body;</span><br><span class="line">    <span class="keyword">const</span> user = <span class="keyword">await</span> ctx.loginAndGetUser(username, password);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 设置 Session</span></span><br><span class="line">    ctx.session.user = user;</span><br><span class="line">    <span class="comment">// 如果用户勾选了 `记住我`，设置 30 天的过期时间</span></span><br><span class="line">    <span class="keyword">if</span> (rememberMe) ctx.session.maxAge = ms(<span class="string">'30d'</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h4 id="延长用户-session-有效期"><a class="markdown-anchor" href="#延长用户-session-有效期">#</a> 延长用户 Session 有效期</h4>
<p>默认情况下，当用户请求没有导致 Session 被修改时，框架都不会延长 Session 的有效期，但是在有些场景下，我们希望用户如果长时间都在访问我们的站点，则延长他们的 Session 有效期，不让用户退出登录态。框架提供了一个 <code>renew</code> 配置项用于实现此功能，它会在发现当用户 Session 的有效期仅剩下最大有效期一半的时候，重置 Session 的有效期。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// config/config.default.js</span></span><br><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">  session: &#123;</span><br><span class="line">    renew: <span class="literal">true</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

  </article>
  <aside id="mobileAside" class="toc">
  <div class="mobile-menu">
    <ul>
      <li><a href="/zh-cn/intro/" alt="指南">指南</a></li><li><a href="/api/" alt="API">API</a></li><li><a href="/zh-cn/tutorials/index.html" alt="教程">教程</a></li><li><a href="https://github.com/search?q=topic%3Aegg-plugin&type=Repositories" alt="插件">插件</a></li><li><a href="https://github.com/eggjs/egg/releases" alt="发布日志">发布日志</a></li>
      
      
        <li class="translations">
          <a class="nav-link">Translations</a>
          <span class="arrow"></span><ul id="dropdownContent" class="dropdown-content"><li><a id="en" href="/en/core/cookie-and-session.html" >English</a></li><li><a id="zh-cn" href="/zh-cn/core/cookie-and-session.html" style="color: #22ab28">中文</a></li></ul>
        </li>
      
    </ul>
  </div>
  <dl><dt id="title-Intro" style="cursor: pointer;" class="aside-title">新手指南<a id="collapse-icon-Intro" class="icon opend"></a></dt><dd id=panel-Intro><ul><li><a href="/zh-cn/intro/index.html" class="menu-link">Egg.js 是什么?</a></li><li><a href="/zh-cn/intro/egg-and-koa.html" class="menu-link">Egg.js 和 Koa</a></li><li><a href="/zh-cn/intro/quickstart.html" class="menu-link">快速入门</a></li><li><a href="/zh-cn/tutorials/progressive.html" class="menu-link">渐进式开发</a></li><li><a href="/zh-cn/migration.html" class="menu-link">2.x 升级指南</a></li></ul></dd><dt id="title-Basics" style="cursor: pointer;" class="aside-title">基础功能<a id="collapse-icon-Basics" class="icon opend"></a></dt><dd id=panel-Basics><ul><li><a href="/zh-cn/basics/structure.html" class="menu-link">目录结构</a></li><li><a href="/zh-cn/basics/objects.html" class="menu-link">内置对象</a></li><li><a href="/zh-cn/basics/env.html" class="menu-link">运行环境</a></li><li><a href="/zh-cn/basics/config.html" class="menu-link">配置</a></li><li><a href="/zh-cn/basics/middleware.html" class="menu-link">中间件</a></li><li><a href="/zh-cn/basics/router.html" class="menu-link">Router</a></li><li><a href="/zh-cn/basics/controller.html" class="menu-link">Controller</a></li><li><a href="/zh-cn/basics/service.html" class="menu-link">Service</a></li><li><a href="/zh-cn/basics/plugin.html" class="menu-link">插件</a></li><li><a href="/zh-cn/basics/schedule.html" class="menu-link">定时任务</a></li><li><a href="/zh-cn/basics/extend.html" class="menu-link">框架扩展</a></li><li><a href="/zh-cn/basics/app-start.html" class="menu-link">启动自定义</a></li></ul></dd><dt id="title-Core" style="cursor: pointer;" class="aside-title">核心功能<a id="collapse-icon-Core" class="icon opend"></a></dt><dd id=panel-Core><ul><li><a href="/zh-cn/core/development.html" class="menu-link">本地开发</a></li><li><a href="/zh-cn/core/unittest.html" class="menu-link">单元测试</a></li><li><a href="/zh-cn/core/deployment.html" class="menu-link">应用部署</a></li><li><a href="/zh-cn/core/logger.html" class="menu-link">日志</a></li><li><a href="/zh-cn/core/httpclient.html" class="menu-link">HttpClient</a></li><li><a href="/zh-cn/core/cookie-and-session.html" class="menu-link">Cookie and Session</a></li><li><a href="/zh-cn/core/cluster-and-ipc.html" class="menu-link">多进程模型和进程间通讯</a></li><li><a href="/zh-cn/core/view.html" class="menu-link">模板渲染</a></li><li><a href="/zh-cn/core/error-handling.html" class="menu-link">异常处理</a></li><li><a href="/zh-cn/core/security.html" class="menu-link">安全</a></li><li><a href="/zh-cn/core/i18n.html" class="menu-link">国际化</a></li></ul></dd><dt id="title-Tutorials" style="cursor: pointer;" class="aside-title">教程<a id="collapse-icon-Tutorials" class="icon opend"></a></dt><dd id=panel-Tutorials><ul><li><a href="/zh-cn/tutorials/mysql.html" class="menu-link">MySQL</a></li><li><a href="/zh-cn/tutorials/restful.html" class="menu-link">RESTful API</a></li><li><a href="/zh-cn/tutorials/passport.html" class="menu-link">Passport 鉴权</a></li><li><a href="/zh-cn/tutorials/socketio.html" class="menu-link">Socket.IO</a></li><li><a href="/zh-cn/tutorials/assets.html" class="menu-link">静态资源</a></li><li><a href="/zh-cn/tutorials/typescript.html" class="menu-link">TypeScript</a></li></ul></dd><dt id="title-Advanced" style="cursor: pointer;" class="aside-title">进阶<a id="collapse-icon-Advanced" class="icon opend"></a></dt><dd id=panel-Advanced><ul><li><a href="/zh-cn/advanced/loader.html" class="menu-link">Loader</a></li><li><a href="/zh-cn/advanced/plugin.html" class="menu-link">插件开发</a></li><li><a href="/zh-cn/advanced/framework.html" class="menu-link">框架开发</a></li><li><a href="/zh-cn/advanced/cluster-client.html" class="menu-link">多进程研发模式增强</a></li><li><a href="/zh-cn/advanced/view-plugin.html" class="menu-link">模板插件开发规范</a></li><li><a href="/zh-cn/style-guide.html" class="menu-link">代码风格指南</a></li></ul></dd><dt id="title-Community" style="cursor: pointer;" class="aside-title">社区<a id="collapse-icon-Community" class="icon opend"></a></dt><dd id=panel-Community><ul><li><a href="/zh-cn/plugins/" class="menu-link">内置插件列表</a></li><li><a href="/zh-cn/contributing.html" class="menu-link">如何贡献</a></li><li><a href="/zh-cn/resource.html" class="menu-link">资源</a></li><li><a href="/zh-cn/faq.html" class="menu-link">常见问题</a></li></ul></dd></dl>
</aside>
<script>
var mobileTrigger = document.getElementById('mobileTrigger');
var mobileAside = document.getElementById('mobileAside');

var expandMenu = function(title) {
  // handle icon
  const collapseIcon = document.getElementById('collapse-icon-' + title);
  if (collapseIcon) {
    collapseIcon.className = 'icon opend';
  }
  // handle panelEle
  const panelEle = document.getElementById('panel-' + title);
  if (panelEle) {
    panelEle.className = '';
  }
}

var collapseMenu = function(title) {
  // handle icon
  const collapseIcon = document.getElementById('collapse-icon-' + title);
  if (collapseIcon) {
    collapseIcon.className = 'icon closed';
  }
  // handle panelEle
  const panelEle = document.getElementById('panel-' + title);
  if (panelEle) {
    panelEle.className = 'aside-panel-hidden';
  }
}

mobileAside.onclick = function(e) {
  const targetId = e.target.id;
  if (targetId && (targetId.indexOf('title-') > -1 || targetId.indexOf('collapse-icon-') > -1)) {
    const title = targetId.replace('title-', '').replace('collapse-icon-', '');
    try { 
      // the the browser may have no localStroage or JSON.parse may throw exception.
      const menuInfo = JSON.parse(window.localStorage.getItem('menuInfo'));
        
      // current menu status
      const curClosed = menuInfo[title] ? menuInfo[title].closed : false; // default false

      // change UI
      curClosed ? expandMenu(title) : collapseMenu(title);

      // save menuInfo to localStorage
      menuInfo[title] = { closed: !curClosed } // opposite
      window.localStorage.setItem('menuInfo', JSON.stringify(menuInfo));
    } catch (e) {}
  }
};

mobileTrigger.onclick = function(e) {
  e.preventDefault();
  if (mobileAside.className.indexOf('mobile-show') === -1) {
    mobileAside.className += ' mobile-show';
  } else {
    mobileAside.className = 'toc';
  }
};

(function() {
  // save data to localStorage because the page will refresh when user change the url.
  let menuInfo;
  try { 
    // the the browser may have no localStroage or JSON.parse may throw exception.
    menuInfo = JSON.parse(window.localStorage.getItem('menuInfo'));
    if (!menuInfo) {
      menuInfo = {};
      window.localStorage.setItem('menuInfo', JSON.stringify(menuInfo));
    }
  } catch (e) {
    menuInfo = {}; // default {}
  }

  for (const title in menuInfo) {
    if (menuInfo[title] && menuInfo[title].closed) { // menu in closed status.
      collapseMenu(title);
    } else {
      expandMenu(title);
    }
  }

  // highlight menu
  const pathname = window.location.pathname;
  const selector = `a[href="${pathname}"].menu-link,a[href="${pathname}index.html"].menu-link`;
  const menuItem = mobileAside.querySelector(selector);
  if (menuItem) { menuItem.className += ' highlight'; }
})();
</script>

</div>

  </div>
</body>
<script src="https://cdn.jsdelivr.net/docsearch.js/2/docsearch.min.js"></script>
<script>
docsearch({
  apiKey: '1561de31a86f79507ea00cdb54ce647c',
  indexName: 'eggjs',
  inputSelector: '#search-query',
});
</script>
<div class="cnzz">
<script src="https://s11.cnzz.com/z_stat.php?id=1261142226&web_id=1261142226" language="JavaScript"></script>
</div>

</html>
